/* ------------------------------------------------
 *
 * Extended Helper for jQuery
 * (c) Roy (roy@kiranatama.com | roy@maysora.com)
 *
 * Dual licensed under the MIT and GPL licenses:
 * - http://www.opensource.org/licenses/mit-license.php
 * - http://www.gnu.org/licenses/gpl.html
 *
 * ------------------------------------------------
 * Requirements:
 * ------------------------------------------------
 *
 * - jQuery js framework
 *
 * ------------------------------------------------
 * Usage:
 * ------------------------------------------------
 *
 * documented individually in every functions
 *
 * ------------------------------------------------
 * Change Log:
 * ------------------------------------------------
 *
 * v.2.4b:
 * - handle jQuery.noConflict() properly
 * - rename $.getClassName(obj) to $.typeOf(obj)
 *
 * v.2.4:
 * - add $.hashToArray(obj)
 * - modify Event.isKeyNumeric() to allow ctrl and alt combination
 * - add String.capitalize()
 * - add $.getClassName(obj)
 *
 * v.2.3:
 * - add $().copyDimension()
 * - add $().focusableToggle()
 * - add $().placeholder()
 * - tidy-up various functions
 * - add Array.delete(obj)
 * - add Array.deleteAt(index)
 * - setInput will not affect already disabled element
 * - add $().defaultVal()
 *
 * v.2.2:
 * - add Array.compare(Array)
 * - add $().returnToTab()
 * - remove eventGetKey(event) use event.which instead
 * - fix setLink onclick typo bug
 *
 * v.2.1:
 * - move deprecated function to separated jquery.extended_helper_compability.js
 *
 * v.2.0:
 * - move most functions to extend either native js object or jQuery
 * - modify some functions to be more jQuery-ish
 * - keep old functions for backward compability for now but don't depend on it =P
 * - add toNumber() to String and Number object
 * - change license to dual license MIT and GPL, commercial use welcome ;P
 *
 * v.1.4:
 * - uiTabsExpireTab()
 *
 * v.1.3:
 * - arrayCompact()
 *
 * v.1.2:
 * - fix resetFields to use resetSelectField for select
 *
 * v.1.1:
 * - sanitize()
 *
 * v.1.0:
 * - serialize, isFieldDirty, setFieldDefault, resetSelectField, focusInput,
 *   resetFields, strToNum, bindNumericOnly, setButton, setLink
 *
 * ------------------------------------------------
 */

/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////

(function($) {
  $.fn.extend({

    /* toggle enable/disable link (or force enable/disable if enabled defined)
     * disabledClass will be added/removed if defined
     * example: $('a.remote').setLink();
     */
    setLink: function(enabled, disabledClass){
      this.filter('a').each(function(){
        var $link = $(this);

        // prepare data container for setLink
        if(!$link.data('setLink'))
          $link.data('setLink', {});
        var linkData = $link.data('setLink');

        var inEnabled = (typeof enabled === 'undefined' || enabled == null) ? !!linkData.disabled : enabled;
        if(inEnabled != !!linkData.disabled) return this;

        if(inEnabled){
          if(!!linkData.oldOnclick)
            this.onclick = linkData.oldOnclick;
          $link.attr('href', linkData.oldHref)
            .removeClass(linkData.disabledClass)
            .data('setLink', {});
        }else{
          if(typeof disabledClass === 'undefined')
            disabledClass = '';
          linkData.oldOnclick = this.onclick;
          this.onclick = '';
          linkData.oldHref = $link.attr('href');
          linkData.disabledClass = disabledClass;
          linkData.disabled = true;
          $link.removeAttr("href")
            .addClass(disabledClass);
        }
        return $link;
      });
      return this;
    },

    /* toggle enable/disable form input (or force enable/disable if enabled defined)
     * disabledClass will be added/removed if defined
     * example: $('form.user :input').setInput()
     */
    setInput: function(enabled, disabledClass){
      this.filter(':input').each(function(){
        var $input = $(this);

        // prepare data container for setInput
        if(!$input.data('setInput'))
          $input.data('setInput', {});
        var inputData = $input.data('setInput');

        var inEnabled = (typeof enabled === 'undefined' || enabled == null) ? !!$input.attr('disabled') : enabled;
        if(inEnabled != !!$input.attr('disabled')) return this;

        if(inEnabled && inputData.disabled)
          $input.removeAttr('disabled')
            .removeClass(inputData.disabledClass)
            .data('setInput', {});
        else if(!$input.is(':disabled')){
          if(typeof disabledClass === 'undefined')
            disabledClass = '';
          inputData.disabledClass = disabledClass;
          inputData.disabled = true;
          $input.attr('disabled', 'disabled')
            .addClass(disabledClass);
        }
        return this;
      });
      return this;
    },

    /* return select default selectedIndex
     * used for $.isInputDirty()
     */
    selectDefaultIndex: function(elem){
      var selectedIndex = -1;
      $('option', elem).each(function(index){
        if(this.defaultSelected){
          selectedIndex = index;
          return false;
        }
        return true;
      });
      return selectedIndex;
    },

    /* return defaultValue for input text, select, radio, checkbox
     * (will not work for input hidden, see http://www.w3.org/Bugs/Public/show_bug.cgi?id=8506)
     */
    defaultVal: function(){
      if(this.is('select')){
        return $('option:eq('+this.selectDefaultIndex()+')', this).val();
      }else if(this.is('input:checkbox') || this.is('input:radio')){
        return this.get(0).defaultChecked;
      }else{
        return this.get(0).defaultValue;
      }
    },

    /* return true if input is dirty (value != defaultValue)
     * example: $(':input').isDirty();
     *          $('select#country').isDirty();
     */
    isDirty: function(){
      var isClean = true;
      this.filter(':input').each(function(){
        var $this = $(this);
        if($this.is('select')){
          isClean = (this.selectedIndex == $this.selectDefaultIndex());
        }else if($this.is('input:checkbox') || $this.is('input:radio')){
          isClean = (this.checked == this.defaultChecked);
        }else{
          isClean = (this.value == this.defaultValue);
        }
        return isClean;
      });
      return !isClean;
    },

    /* set object's default value to current value
     * example: $('form#user_form :input').setDefault();
     */
    setDefault: function(){
      this.filter(':input').each(function(){
        var $this = $(this);
        if($this.is('select')){
          $this.find('option').each(function(){
            this.defaultSelected = this.selected;
          });
        }else if($this.is('input:checkbox') || $this.is('input:radio')){
          this.defaultChecked = this.checked;
        }else{
          this.defaultValue = this.value;
        }
      });
      return this;
    },

    /* reset input to its default value
     * Notice that for some reason hidden input can't be resetted using defaultValue.. except in IE..
     *   http://www.w3.org/Bugs/Public/show_bug.cgi?id=8506
     * example: $(':input').resetInput();
     */
    resetInput: function(){
      this.filter(':input').each(function(){
        var $this = $(this);
        if($this.is('select'))
          $this.find('option').each(function(){
            this.selected = this.defaultSelected;
          });
        else
          $this.attr("value",function(){return this.defaultValue;});
      });
      return this;
    },

    /* focus first input (or blank input if blankFirst true) inside
     * object defined by selector (or document if selector not defined)
     * then select (block) its content if select true
     * example: $(document).focusInput();
     *          $('div.container').focusInput(true);
     *          $(':input').focusInput(true, true);
     */
    focusInput: function(blankFirst, select){
      var e = null;
      var e2 = null;
      this.find(':input:visible:enabled:not(.no_focus)').andSelf().filter(':input').each(function(){
        if(!e2)
          e2 = this;
        if($(this).val() == ''){
          e = this;
          return false;
        }
        return true;
      });
      if(e && blankFirst){
        e.focus();
        if(select) e.select()
      }else if(e2){
        e2.focus();
        e2.value = e2.value;
        if(select) e2.select()
      }
      return this;
    },

    /* bind input so only numbers can be inputted
     * options:
     * * onlyPositive: true/false, disallow minus sign if true
     * * onlyInteger: true/false, disallow period (.) if true
     * * minValue: number, only allow value greater or equal to specified number
     * * maxValue: number, only allow value lower or equal to specified number
     * * allowBlank: true/false, allow/disallow blank value
     * example:
     * * $(':input.numeric').numericOnly();
     * * $(':input.numeric.positive').numericOnly({onlyPositive: true});
     * * $('#user_age').numericOnly({onlyPositive: true, onlyInteger: true, minValue: 13, maxValue: 100, allowBlank: false})
     */
    numericOnly: function(uoptions){
      var options = {
        onlyPositive: false,
        onlyInteger: false,
        minValue: null,
        maxValue: null,
        allowBlank: true
      }
      if(typeof uoptions !== 'undefined')
        $.extend(options, uoptions);

      this.filter(':input').each(function(){
        var $this = $(this);

        $this
          .data('numericOnly', options)
          .bind('keydown', function(e){
            var options = $(this).data('numericOnly');
            return e.isKeyNumeric(options.onlyPositive, options.onlyInteger);
          })
          .bind('blur', function(e){
            var $obj = $(e.target);
            var options = $obj.data('numericOnly');
            if($obj.val() != '' || !options.allowBlank){
              if(options.minValue && ($obj.val().toNumber() < options.minValue.toNumber()))
                $obj.val(options.minValue);
              else if(options.maxValue && ($obj.val().toNumber() > options.maxValue.toNumber()))
                $obj.val(options.maxValue);
              else
                $obj.val($obj.val().toNumber());
            }
            $obj.trigger('keyup');
          });

        if($this.val() == '' && !options.allowBlank)
          $this.blur();
      });
      return this;
    },

    /* bind elements, change return key event to tab event
     * example:
     * $('input:text,input:password,input:checkbox,input:radio').returnToTab();
     */
    returnToTab: function(){
      return this.keypress(function(event){
        if(event.which == 13){
          event.preventDefault();
          var focusableElements = $(':input,a,:button').filter(':visible:not(:disabled)');
          var operand = event.shiftKey ? -1 : 1;
          var nextElem = focusableElements.eq(focusableElements.index(this)+operand);
          if(nextElem.length) $(this).blur();
          nextElem.is(':text,:password,textarea') ? nextElem.select() : nextElem.focus();
        }
      });
    },

    /* copy element width and height from source element
     *  including margin, padding and border
     */
    copyDimension: function(source){
      this.css({
        'width': $(source).outerWidth(true),
        'height': $(source).outerHeight(true)
      });
      return this
    },

    /* toggle hide/show element but retaining focusable capability
     */
    focusableToggle: function(show){
      if(typeof show === 'undefined')
        show = this.css('opacity') == 0;
      if(show){
        var data = this.css('opacity', 1).data('focusableToggleData');
        if(data) this.css(data);
      }else if(this.css('opacity') != 0){
        this.data('focusableToggleData', {width:this.width(), height:this.height()})
          .css({'opacity': 0,'width': 1,'height': 1});
      }
    },

    /* add placeholder text
     * options:
     * - type:
     * - 1 default, will add placeholder text inside the input
     * - 2 will replace input with placeholder text only if value is blank
     * - 3 similar to type 2 but will replace input even when value not blank,
     *     but the placeholder will be the inputted text
     * - icon: only for type 3, add additional text in placeholder from value
     * example:
     * - $(':input.placeholder').placeholder('Please enter your name', {
     *     type: 2,
     *     icon: '<img src="editable.png" alt="editable"></img>'
     *   });
     */
    placeholder: function(text, uoptions){
      this.each(function(){
        var $this = $(this).filter(':not(.placeholder_input)');
        if($this.length && typeof text !== 'undefined' && text && text.length){
          var options = {
            type: 1,
            icon: ''
          };
          if(typeof uoptions !== 'undefined') $.extend(options, uoptions);
          $this.after('<div class="placeholderText">'+text+'</div>');
          var $placeholder = $this.next(), focusFunction, blurFunction;

          $placeholder.css({
            'display': 'none',
            'margin-top': ($this.outerHeight(true) - $placeholder.css('font-size').toNumber()) / 2,
            'margin-left': 7,
            'top': $this.position().top,
            'left': $this.position().left,
            'position': 'absolute',
            'font-size': $this.css('font-size'),
            'color': $this.css('color')
          });
          if(options['type'] > 1){
            $this.wrap('<div class="placeholderWrapper"></div>');
            var $wrapper = $this.parent();
            $placeholder.css('cursor', 'pointer');
          }
          switch(options['type']){
            case 2:
              $placeholder.copyDimension(this);
              focusFunction = function(){
                $this.focusableToggle(true);
                $placeholder.hide();
                $wrapper.copyDimension($this);
              };
              blurFunction = function(){
                if($this.val() == ''){
                  $this.focusableToggle(false);
                  $placeholder.show();
                  $wrapper.copyDimension($placeholder);
                }
              };
              break;
            case 3:
              focusFunction = function(){
                $this.focusableToggle(true);
                $placeholder.hide();
                $wrapper.copyDimension($this);
              };
              blurFunction = function(){
                $this.focusableToggle(false);
                $placeholder.show().html(
                  (!$this.val() || $this.val() == '') ? text :
                    $this.val().sanitize().replace(/\n/g,'<br />\n') + options['icon']
                );
                $wrapper.copyDimension($placeholder);
              };
              break;
            default:
              focusFunction = function(){$placeholder.hide();};
              blurFunction = function(){if(!$this.val() || $this.val()== '') $placeholder.show();};
              break;
          }
          $placeholder.click(function(){$this.focus();});
          $this.focus(focusFunction)
            .blur(blurFunction)
            .addClass('placeholder_input')
            .triggerHandler('blur');
        }
      });
      return this;
    }
  });

  /* Force expire cached tab with specified index
   * require: jQuery UI Tabs
   * example:
   * - uiTabsExpireTab($('#tabs'), 0)  // will expire tab with index 0 (first tab)
   */
  function uiTabsExpireTab(tabs, index){
    $.data($('li a', tabs).get(index), 'cache.tabs', false);
    $($.data($('li a', tabs).get(index)).handle.elem.hash).html('');
  }

  /////////////////////////////////////////////////////////////////////////////////////
  /////////////////////////////////////////////////////////////////////////////////////
  /////////////////////////////////////////////////////////////////////////////////////

  /* make sure String object support trim function */
  if(typeof(String.prototype.trim) === "undefined"){
    String.prototype.trim = function(){
      return String(this).replace(/^\s+|\s+$/g, '');
    };
  }

  /* extend Array object */
  $.extend(Array.prototype, {
    /* add compact function to Array */
    compact: function(remove_level){
      var new_array = new Array();
      for(var k=0; k < this.length; k++){
        if(typeof(this[k]) !== 'undefined' &&
            this[k] != null &&
            !(remove_level > 0 &&
            (typeof(this[k]) === 'string' || remove_level > 1) &&
            this[k] == ''))
          new_array.push(this[k]);
      }
      return new_array;
    },
    /* compare 2 array return true if both array have same content */
    compare: function(other_arr){
      if($.typeOf(other_arr) != "Array" || this.length != other_arr.length)
        return false;
      for(var i=0; i < this.length; i++)
        if(this[i] != other_arr[i]) return false;
      return true;
    },
    /* Deletes items from self that are equal to obj. If the item is not found, returns null. */
    'delete': function(obj){
      if(this.indexOf(obj) < 0) return null;

      for(var k = 0; k < this.length;)
        if(this[k] == obj)
          this.deleteAt(k);
        else
          k++;
      return obj;
    },
    /* Deletes the element at the specified index, returning that element, or nil if the index is out of range. */
    deleteAt: function(index){
      return this.splice(index,1)[0];
    }
  });

  /* extend String object */
  $.extend(String.prototype, {
    /* convert string to Number, using ruby to_i behavior */
    toNumber: function(){
      var num = Number(this.replace(/\D+$/g, ''));
      return (isNaN(num)) ? 0 : num;
    },

    /* sanitize text to prevent xss and sql injection */
    sanitize: function(){
      return this.replace(/(\\|<\/|>|\r\n|[\n\r"'])/g,'');
    },

    capitalize: function(){
      return this.replace(/^[a-z]/, function($0){return $0.toUpperCase();});
    }
  });

  /* extend Number object */
  $.extend(Number.prototype, {
    /* return self */
    toNumber: function(){
      return Number(this);
    }
  });

  /* extend Event object */
  $.extend(jQuery.Event.prototype, {
    /* return false if event pressed key not a number
     * used in numericOnly
     */
    isKeyNumeric: function(forbidMinus, forbidPeriod){
      var iKeyCode = this.which;

      if(this.ctrlKey || this.altKey) return iKeyCode; // allow ctrl and alt key combination

      switch(iKeyCode){
        case 8: case 9: case 35: case 36: case 37: case 38: case 39: case 40: case 45: case 46:
          break;
        case 48: case 49:case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57:
          if (this.shiftKey) return false;
          break;
        case 96: case 97: case 98: case 99: case 100: case 101: case 102: case 103: case 104: case 105:
          //return correct numeric from keypad
          return iKeyCode - 48;
          break;
        case 109: case 110: case 189: //minus sign
          if(forbidMinus) return false;
          break;
        case 190: //period
          if(forbidPeriod) return false;
          break;
        default:return false;
      }
      return iKeyCode
    }
  });

  /* return string of object type
   * usage: $.typeOf(obj);
   */
  $.typeOf = function(obj){
    try{
      var constructorStr = obj.constructor.toString();
      var tempMatch = constructorStr.match(/function (.+)\(/);
      if(!tempMatch || tempMatch.length != 2) tempMatch = constructorStr.match(/ (.+)\]/);
      return tempMatch[1];
    }catch(e){
      return (typeof obj).capitalize();
    }
  }

  /* convert hash table to array
   * usage: $.hashToArray(hash);
   */
  $.hashToArray = function(obj){
    var tempArr = new Array();
    for(key in obj){
      tempArr.push([key, obj[key]]);
    }
    return tempArr;
  };
})(jQuery);
